Interactive data visualizations in R: WAIAI on-demand workshop

Introduction

What are interactive plots, and what are they used for?

Interactive plots are dynamic data visualizations that and respond to viewers’ actions (e.g., clicking, hovering).

Interactive plots can be used to…

  • Help audiences quickly digest information in complex data visualizations

  • Support data-driven decision-making with enhanced synthesis

  • Facilitate stakeholder communication and collaboration

  • Enhance strategic clarity, particularly with complex data (e.g., many groups)

R is a very useful tool for data visualization because it can be used to easily integrate interactive visualizations directly into deliverables, from reports to data dashboards. R is also accessible and open-source with many well-documented packages that can be used to create custom interactive visualizations.

How do we create compelling interactive plots in R?

  1. Define question to be addressed and explore the data

  2. Create static versions of each data visualizations following data visualization principles

Plot types we’ll examine today include:

  • Line plots

  • Bar plots

  • Rank plots

  • Scatter plots

  1. Use common R packages, plotly (Sievert, 2020) and ggiraph (Gohel & Skintzos, 2025), to make custom interactive plots

Load needed packages

See all software references at the end of the tutorial.

# load packages and describe their uses
library(tidyverse) # data cleaning, organization, and visualization
library(plotly) #key interactive plot package
library(ggiraph) #key interactive plot package
library(MetBrewer) #plot customization functions
library(tidytext) #plot customization functions
library(ggbump) #plot customization functions
library(patchwork) #plot customization functions

#color palette
MetBrew_Signac <- MetBrewer::met.brewer("Signac", n = 21)

## install packages by running install.packages("PACKAGE-NAME")
## cite packages by running citation(package = "PACKAGE-NAME")
## load packages by running library(PACKAGE-NAME)
## specify function from a specific package by running PACKAGE-NAME::function()
## troubleshoot functions from packages by running ?PACKAGE-NAME::function()

Define question to be addressed and explore data

Question to be addressed: For this tutorial, we will be examining performance and attendance of women’s soccer/football teams, and how they might be related, in the top-tier league in the United Kingdom.

Data: We will looking at English Women’s Football data from the EWF database (The English Women’s Football Database, 2025), which is available on Github at the following URL: https://github.com/probjects/ewf-database. You can download the data and access the data dictionary directly at the database link.

#import matches dataset, one row per game per season
df_matches <- read_csv("./data/ewf_matches.csv") %>%
  map_df(., ~ gsub("Women", "", .x, fixed = T)) %>%
  map_df(., ~ gsub("Ladies", "", .x, fixed = T)) %>%
  map_df(., ~ gsub("Belles", "", .x, fixed = T)) #remove parts of team names since they're used inconsistently, e.g., Arsenal = Arsenal Ladies

#examine data
head(df_matches)
## # A tibble: 6 × 22
##   season_id       season    tier  division  match_id match_name date  attendance
##   <chr>           <chr>     <chr> <chr>     <chr>    <chr>      <chr> <chr>     
## 1 S-2011-2011-1-S 2011-2011 1     FA 's Su… M-2011-… "Chelsea … 2011… 2510      
## 2 S-2011-2011-1-S 2011-2011 1     FA 's Su… M-2011-… "Lincoln … 2011… 742       
## 3 S-2011-2011-1-S 2011-2011 1     FA 's Su… M-2011-… "Birmingh… 2011… 602       
## 4 S-2011-2011-1-S 2011-2011 1     FA 's Su… M-2011-… "Liverpoo… 2011… 835       
## 5 S-2011-2011-1-S 2011-2011 1     FA 's Su… M-2011-… "Everton … 2011… 220       
## 6 S-2011-2011-1-S 2011-2011 1     FA 's Su… M-2011-… "Liverpoo… 2011… 341       
## # ℹ 14 more variables: home_team_id <chr>, home_team_name <chr>,
## #   away_team_id <chr>, away_team_name <chr>, score <chr>,
## #   home_team_score <chr>, away_team_score <chr>, home_team_score_margin <chr>,
## #   away_team_score_margin <chr>, home_team_win <chr>, away_team_win <chr>,
## #   draw <chr>, result <chr>, note <chr>
#import standings dataset, one row per team per season
df_standings <- read_csv("./data/ewf_standings.csv") %>%
  map_df(., ~ gsub("Women", "", .x, fixed = T)) %>%
  map_df(., ~ gsub("Ladies", "", .x, fixed = T)) %>%
  map_df(., ~ gsub("Belles", "", .x, fixed = T)) 

#examine data
head(df_standings)
## # A tibble: 6 × 17
##   season_id  season tier  division position team_id team_name played wins  draws
##   <chr>      <chr>  <chr> <chr>    <chr>    <chr>   <chr>     <chr>  <chr> <chr>
## 1 S-2011-20… 2011-… 1     FA 's S… 1        T-001-T "Arsenal… 14     10    2    
## 2 S-2011-20… 2011-… 1     FA 's S… 2        T-003-T "Birming… 14     8     5    
## 3 S-2011-20… 2011-… 1     FA 's S… 3        T-013-T "Everton… 14     7     4    
## 4 S-2011-20… 2011-… 1     FA 's S… 4        T-016-T "Lincoln… 14     6     3    
## 5 S-2011-20… 2011-… 1     FA 's S… 5        T-006-T "Bristol… 14     4     4    
## 6 S-2011-20… 2011-… 1     FA 's S… 6        T-008-T "Chelsea… 14     4     3    
## # ℹ 7 more variables: losses <chr>, goals_for <chr>, goals_against <chr>,
## #   goal_difference <chr>, points <chr>, point_adjustment <chr>,
## #   season_outcome <chr>

Attendance

Static line plot with ggplot2

attendanceplot <- df_matches %>% #data organization
  subset(tier == 1 & season != "2020-2021") %>% #select top tier and exclude 2020 season
  select(c(home_team_name, attendance, match_name, date, season)) %>% #select only columns of interest
  group_by(season, home_team_name) %>%
  mutate(mean_attendance = mean(as.numeric(attendance), na.rm = T)) %>% #add average attendance column, grouped by season and team
  ungroup() %>% 
  select(c(home_team_name, mean_attendance, season)) %>% #select needed columns only
  distinct() %>%
ggplot(aes(x = as.factor(season), y = as.numeric(mean_attendance), group=as.factor(home_team_name),  color=as.factor(home_team_name))) + #set axes and groups
  geom_point(size = 2) + #add points to plot
  geom_path(inherit.aes = TRUE) + #add lines to plot
  scale_color_manual(values= MetBrew_Signac, name = "Team") + #set color palette
  theme_classic() + #set theme
  labs(title = "Average game attendance per home team per season", #add axis labels, caption, and titles/subtitles
       subtitle = "English Football Women's Super League (tier 1)",
       x = "Season",  y = "Average game attendance",
       caption = "Source: EWF Database (2025), https://github.com/probjects/ewf-database. Excludes 2020-2021.") +
    theme(legend.position="bottom", #put legend on the bottom 
          legend.key.size = unit(0.05, "cm")) + #decrease size between legend items
    theme(legend.text = element_text(size = 10)) + #change legend font size
    theme(axis.text.x = element_text(angle = 45, vjust = 0.75, hjust = 0.75)) #rotate season labels so they don't overlap
attendanceplot

Interactive line plot with plotly: ggplotly() shortcut

plotly::ggplotly(attendanceplot)

Interactive line plot with plotly: plot_ly() longform

#same data organization as static version
plotly_df <- df_matches %>%
  subset(tier == 1 & season != "2020-2021") %>%
  select(c(home_team_name, attendance, match_name, date, season)) %>% #select only columns of interest
  group_by(season, home_team_name) %>%
  mutate(mean_attendance = mean(as.numeric(attendance), na.rm = T)) %>% #add average attendance column, grouped by season and team
  ungroup() %>%
  select(c(home_team_name, mean_attendance, season)) %>% #select only columns of interest
  distinct()

attendanceplotly <- plotly::plot_ly(data = plotly_df, x = ~as.factor(season), y = ~mean_attendance, split = ~home_team_name, color = ~home_team_name, #set axes in plotly
    type = 'scatter', #set plot type
    mode = 'lines+markers', #choose items to include in scatter plot
    text = ~paste("Home team:", home_team_name, "Average attendance:", mean_attendance), #set hover labels
    hoverinfo = 'text',
    marker = list(size = 6), #set dot size
    line = list(width = 2) #set line size
  ) %>%
  layout( #set titles, legend, and axis labels
    title = "Average game attendance per season per home team (double click in legend)", 
    subtitle = "English Football Women's Super League (tier 1), Source: EWF Database (2025), https://github.com/probjects/ewf-database. Excludes 2020-2021.",
    hovermode = "closest",
    legend = list(itemclick = "toggleothers"),
    xaxis = list(title = "Season"),
    yaxis = list(title = "Average game attendance")
  ) 
attendanceplotly

Interactive line plot with ggiraph

#use most of same plot code as static version, but make point and line interactive with ggiraph
attendanceplot_giraph <- df_matches %>%
  subset(tier == 1 & season != "2020-2021") %>% #select top tier and exclude 2020 season
  select(c(home_team_name, attendance, match_name, date, season)) %>% #select only columns of interest
  group_by(season, home_team_name) %>% 
  mutate(mean_attendance = round(mean(as.numeric(attendance), na.rm = T), 2)) %>% #add average attendance column, grouped by season and team
  ungroup() %>%
  select(c(home_team_name, mean_attendance, season)) %>% #select needed columns only
  distinct() %>%
ggplot(aes(x = as.factor(season), y = as.numeric(mean_attendance), group=as.factor(home_team_name),  color=as.factor(home_team_name), #set axes and color
           tooltip=paste("Team:", home_team_name, "<br>Average attendance:", mean_attendance), #text for interactive hover
           data_id = as.factor(home_team_name))) + #set data id for hover
  geom_point_interactive(size = 2, hover_nearest = TRUE) + #add interactive points to plot
  geom_line_interactive() + #add interactive lines to plot
  scale_color_manual(values= MetBrew_Signac, name = "Team") + #set color palette
  theme_classic() + #set theme
  labs(title = "Average game attendance per home team per season", #add labels for title/subtitle, caption, and axes
       subtitle = "English Football Women's Super League (tier 1)",
       x = "Season",  y = "Average game attendance",
       caption = "Source: EWF Database (2025), https://github.com/probjects/ewf-database. Excludes 2020-2021.") +
    theme(legend.position="bottom",
          legend.key.size = unit(0.05, "cm"),
          legend.text = element_text(size = 7)) + #adjust positioning for legend, font size
    theme(axis.text.x = element_text(angle = 45, vjust = 0.75, hjust = 0.75)) #rotate season labels so they don't overlap


css_dot_hover <- girafe_css_bicolor(primary = "aquamarine", secondary = "blue") #set colors to be used for opts_hover
attendanceplot_giraphplot <- girafe(ggobj = attendanceplot_giraph, options = list(
                             opts_hover = opts_hover(css = css_dot_hover), #customize fill and outline of hover
                             opts_tooltip = opts_tooltip(css = "padding:3px;background-color:white;color:black;"), #customize size and colors of text label and background when hovering
                              opts_sizing(rescale = TRUE, width = 1) #customize resizing
                )
              )
attendanceplot_giraphplot

Performance

Total goal differential per team per season

Static diverging barplot with ggplot2
goalsdiffplot <- df_standings %>%
  subset(tier == 1) %>% #subset only top tier
  select(c(team_name, goal_difference, goals_for, goals_against, season)) %>% #select columns of interest
  dplyr::group_by(season) %>%
  dplyr::arrange(as.numeric(goal_difference)) %>% #reorder by goal difference within season
  dplyr::ungroup() %>%
  ggplot(aes(x = reorder(as.factor(team_name), as.numeric(goal_difference)), y = as.numeric(goal_difference), fill=as.factor(team_name), #set x and y axes, and group by team for plot
             goal_difference_text = goal_difference, team_text = team_name)) + #text for ggplotly labels
  scale_fill_manual(values=c(MetBrew_Signac), name = "Team name") + #set fill palette
  scale_color_manual(values=c(MetBrew_Signac), name = "Team name") + #set color palette
  geom_bar(stat = "identity") + #add bar for barplot
  theme_classic() + #set theme 
  coord_flip() + #flip X and Y axis
  facet_wrap(~season, scales = "free")+ #create facets by season
  geom_hline(yintercept = 0, linewidth = 0.5, color = "gray") + #add line at 0 (no goal difference)
  labs(title = "Total goal difference per team per season", #add axis labels, caption, title/subtitle
      subtitle = "English Football Women's Super League (tier 1)",
       caption = "Source: EWF Database (2025), https://github.com/probjects/ewf-database",
    y = "Goal difference (for-against)", x = "Team") +
  theme(legend.position="none")
goalsdiffplot

Interactive diverging barplot with plotly (ggplotly())
ggplotly(goalsdiffplot,
         width = 1000, #adjust width
         height = 800, #adjust height
         tooltip = c("team_text", "goal_difference_text") #add hover labels
         ) 
Interactive diverging barplot with ggiraph
#use most of same plot code as static version, but make bar interactive with ggiraph
goalsdiff_giraph <- df_standings %>%
  subset(tier == 1) %>% #subset only top tier
  select(c(team_name, goal_difference, goals_for, goals_against, season)) %>% #select only columns of interest
  dplyr::group_by(season) %>%
  dplyr::arrange(as.numeric(goal_difference)) %>%
  dplyr::ungroup() %>%
  ggplot(aes(x = reorder(as.factor(team_name), as.numeric(goal_difference)), y = as.numeric(goal_difference), fill=as.factor(team_name),
             tooltip=paste("Team:", team_name, "<br>Total goal difference:", goal_difference), #text for interactive hover
             data_id = as.factor(team_name))) +
  scale_fill_manual(values=c(MetBrew_Signac), name = "Team name") + #set fill palette
  scale_color_manual(values=c(MetBrew_Signac), name = "Team name") + #set color palette
  geom_bar_interactive(stat = "identity") + #add interactive bar
  theme_classic() + #set theme
  coord_flip() + #flip x and y axis
  facet_wrap(~season, scales = "free")+ #facet by season
  geom_hline(yintercept = 0, linewidth = 0.5, color = "gray") +
  labs(title = "Total goal difference (for-against) per team per season", #add title, subtitle, caption, x and y axis labels
      subtitle = "English Football Women's Super League (tier 1)",
       caption = "Source: EWF Database (2025), https://github.com/probjects/ewf-database",
    y = "Goal difference (for-against)", x = "Team") +
  theme(legend.position="none") #remove legend

goalsdiffggiraph_plot <- girafe(ggobj = goalsdiff_giraph,
                                width_svg = 12,  #adjust width
                                height_svg = 8, #adjust height
                                options = list(
                               opts_hover(css = "fill:aquamarine;stroke:black;cursor:pointer;"))) #customize fill and outline of hover
goalsdiffggiraph_plot

Total points earned per team per season

Static stacked frequency barplot with ggplot2
pointsplot <- df_standings %>%
  subset(tier == 1) %>% #subset only top tier
  select(c(team_name, points, season)) %>% #select only columns of interest
  ggplot(aes(x=as.factor(season), y=as.numeric(points), fill=as.factor(team_name), #set x and y axes
                          points_text = points, team_text = team_name)) + #text for ggplotly labels
  geom_bar(stat="identity", position = "stack") +
  scale_fill_manual(values=c(MetBrew_Signac), name = "Team name") + #set fill palette
  scale_color_manual(values=c(MetBrew_Signac), name = "Team name") + #set color palette
  labs(title="Points per team by season", #add title, subtitle, caption, x and y axis labels
       subtitle = "English Football Women's Super League (tier 1)",
       caption = "Win = 3 points, Draw = 1 point, Loss = 0 points. Source: EWF Database (2025), https://github.com/probjects/ewf-database",
       x="Season", y = "Points") + 
  theme_classic()  + 
  theme(legend.position="bottom",
          legend.key.size = unit(0.05, "cm")) + #put legend on the bottom and decrease size between items
  theme(legend.text = element_text(size = 10)) + #change legend font size
  theme(axis.text.x = element_text(angle = 45, vjust = 0.75, hjust = 0.75)) #rotate season labels so they don't overlap
pointsplot

Interactive stacked barplot with plotly (ggplotly())
ggplotly(pointsplot,
         width = 1000, #adjust width 
         height = 800, #adjust height 
         tooltip = c("team_text", "points_text"))
Interactive stacked frequency barplot with ggiraph
points_ggiraph <- df_standings %>%
  subset(tier == 1) %>% #subset only top tier
  select(c(team_name, points, season)) %>% #select only columns of interest
  ggplot(aes(x=as.factor(season), y=as.numeric(points), fill=as.factor(team_name), #set x and y axes
             tooltip=paste("Team:", team_name, "<br>Season total points:", points), #text for interactive hover
             data_id = as.factor(team_name))) + 
  geom_bar_interactive(stat="identity", position = "stack") + #add interactive bar
  scale_fill_manual(values=c(MetBrew_Signac), name = "Team name") + #set fill palette
  scale_color_manual(values=c(MetBrew_Signac), name = "Team name") + #set color palette
  labs(title="Points per team by season", #add title, subtitle, caption, x and y axis labels
       subtitle = "English Football Women's Super League (tier 1)",
       caption = "Win = 3 points, Draw = 1 point, Loss = 0 points. Source: EWF Database (2025), https://github.com/probjects/ewf-database",
       x="Season", y = "Points") + 
  theme_classic()  + #set theme
  theme(legend.position="bottom",
          legend.key.size = unit(0.05, "cm")) + #put legend on the bottom and decrease size between items
  theme(legend.text = element_text(size = 7)) + #change legend font size
  theme(axis.text.x = element_text(angle = 45, vjust = 0.75, hjust = 0.75)) #rotate season labels so they don't overlap

pointsggiraph_plot <- girafe(ggobj = points_ggiraph, 
                           options = list(
                             opts_hover(css = "fill:magenta;stroke:pink;cursor:pointer;"), #customize fill and outline of hover
                             opts_tooltip = opts_tooltip(css = "padding:2px;background-color:white;color:black;"), #customize size and colors of text label and background when hovering
                            opts_sizing(rescale = TRUE, width = 1) #customize resizing
                           )
     )
pointsggiraph_plot

Team final standings per season

Static rank plot with ggplot2
rankplot <- df_standings %>%
  subset(tier == 1) %>% #subset only top tier
  select(c(team_name, position, season)) %>% #select only columns of interest
  ggplot(aes(x = as.factor(season), y = as.numeric(position), color = as.factor(team_name), group = as.factor(team_name), fill = as.factor(team_name), 
             team_text = team_name, rank_text = position)) + #text for ggplotly labels
  geom_bump(size = 2) + #add bump plot lines for standings
  scale_y_reverse(breaks = c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)) + #flip y axis scale so top teams appear at top
  geom_point(size = 4)  + #add points at rank per season
  scale_fill_manual(values=c(MetBrew_Signac), name = "Team name") + #set fill palette
  scale_color_manual(values=c(MetBrew_Signac), name = "Team name") + #set color palette
  theme_classic() + #set plot theme
  labs(y = "Final position", x = "Season",  #add title, subtitle, caption, x and y axis labels
       title = "Final position per team by season",
       subtitle = "English Football Women's Super League (tier 1)",
       caption = "Missing 2020-2021. Source: EWF Database (2025), https://github.com/probjects/ewf-database") +
    theme(legend.position="bottom",
          legend.key.size = unit(0.05, "cm")) + #put legend on the bottom and decrease size between items
    theme(legend.text = element_text(size = 10)) + #change legend font size
    theme(axis.text.x = element_text(angle = 45, vjust = 0.75, hjust = 0.75)) #rotate season labels so they don't overlap
rankplot

Interactive rank plot with plotly (ggplotly())
plotly::ggplotly(rankplot,
         width = 1000, #adjust width
         height = 800, #adjust height 
         tooltip = c("team_text", "rank_text") #customize hover labels
         )

Analysis: associations between game attendance and performance

Average season game attendance and past season performance (prior season total points earned)

Static scatterplot with ggplot2

attendance_df <- df_matches %>%
  subset(tier == 1) %>% #subset only top tier
  select(c(home_team_name, attendance, match_name, date, season)) %>% #select only columns of interest
  group_by(season, home_team_name) %>%
  mutate(mean_attendance = mean(as.numeric(attendance), na.rm = T)) %>% #add average attendance column, grouped by season and team
  ungroup() %>%
  select(c(home_team_name, mean_attendance, season)) %>% #select only columns of interest
  distinct() %>%
  dplyr::rename("team_name" = "home_team_name") %>%
  left_join(., df_standings) 
  
scatterplot1 <- attendance_df %>%
  dplyr::group_by(team_name) %>%
  mutate(lagged_points = lag(as.numeric(points), n = 1)) %>% # create lagged points column from prior season
  ungroup() %>%
  subset(season != "2020-2021") %>% #remove 2020-2021, no attendance data due to lockdowns
  select(c(team_name, mean_attendance, lagged_points, season)) %>% #select only columns of interest
    ggplot(aes(y=as.numeric(mean_attendance), x = as.numeric(lagged_points))) +  #choose x and y axes, color 
    geom_point(aes(color = team_name), size = 2) + #add points to plot
    scale_fill_manual(values= MetBrew_Signac, name = "Team") + #set fill palette
    scale_color_manual(values= MetBrew_Signac, name = "Team") + #set color palette
    geom_smooth(color = "black") + #add smoothed line
    labs(x = "Prior year points",  y = "Average game attendance", #add title, subtitle, caption, x and y axis labels
       title = "Association between average game attendance and prior year league points",
       subtitle = "English Football Women's Super League (tier 1)",
       caption = "Source: EWF Database (2025), https://github.com/probjects/ewf-database. Missing 2020-2021. Win = 3 points, Draw = 1 point, Loss = 0 points.") +
    theme_classic() + #set theme
    theme(legend.position="bottom",
          legend.key.size = unit(0.05, "cm")) + #put legend on the bottom and decrease size between items
    theme(legend.text = element_text(size = 10)) #change legend font size
scatterplot1

Interactive scatterplot with plotly (ggplotly())

plotly::ggplotly(scatterplot1)

Interactive scatterplot with ggiraph

scatterplot1_giraph <- attendance_df %>%
    dplyr::group_by(team_name) %>%
  mutate(lagged_points = lag(as.numeric(points), n = 1)) %>% # create lagged points column from prior season
  ungroup() %>%
    subset(season != "2020-2021") %>% #remove 2020-2021, no attendance data due to lockdowns
  select(c(team_name, mean_attendance, lagged_points, season)) %>% #select only columns of interest
    ggplot(aes(y=as.numeric(mean_attendance), x = as.numeric(lagged_points),
          tooltip=paste("Team:", team_name, "<br>Season:", season, "<br>Mean attendance:", mean_attendance, "<br>Prior year points:", lagged_points), #text for interactive hover
             data_id = as.factor(team_name))) +  #choose x and y axes, color
    geom_point_interactive(aes(color = team_name), size = 2) + #add points to plot
    scale_fill_manual(values= MetBrew_Signac, name = "Team") + #set fill palette
    scale_color_manual(values= MetBrew_Signac, name = "Team") + #set color palette
    geom_smooth_interactive(aes(tooltip = "smoothed line", data_id = "smooth_line"), method = "loess", se = TRUE, color = "black") + #add interactive line
    labs(x = "Prior year points",  y = "Average game attendance", #add title, subtitle, caption, x and y axis labels
       title = "Association between average game attendance and prior year league points",
       subtitle = "English Football Women's Super League (tier 1)",
       caption = "Source: EWF Database (2025), https://github.com/probjects/ewf-database. Missing 2020-2021. Win = 3 points, Draw = 1 point, Loss = 0 points.") +
    theme_classic() + #set theme
    theme(legend.position="bottom",
          legend.key.size = unit(0.05, "cm")) + #put legend on the bottom and decrease size between items
    theme(legend.text = element_text(size = 8)) #change legend font size

css_dot_hover <- girafe_css_bicolor(primary = "aquamarine", secondary = "blue") #set colors to be used for opts_hover
scatterplot1_giraphplot <- girafe(ggobj = scatterplot1_giraph, options = list(
                             opts_hover = opts_hover(css = css_dot_hover), #customize fill and outline of hover
                             opts_tooltip = opts_tooltip(css = "padding:3px;background-color:white;color:black;"), #customize size and colors of text label and background when hovering
                              opts_sizing(rescale = TRUE, width = 1) #customize resizing
                )
              )
scatterplot1_giraphplot

Multi-panel interactive plots

#reconfigure legends and labels to fit into one multiplot
scatterplot1_giraph <- scatterplot1_giraph + theme(legend.position="bottom")  +
  labs(caption = "",
       title = "Teams with better outcomes in a season had more fan attendance in the next.") #fix labels

points_ggiraph <- points_ggiraph + theme(legend.position="none") + 
  labs(title = "Total points by season", y = "Total points", subtitle = "", caption = "Source: EWF Database (2025), https://github.com/probjects/ewf-database") #fix labels

attendanceplot_giraph <- attendanceplot_giraph + theme(legend.position="none") +
  labs(title = "Mean attendance by season", y = "Mean attendance", subtitle = "", caption = "") #fix labels

multipanels <- scatterplot1_giraph / (attendanceplot_giraph + points_ggiraph) #create multi-panel plot with patchwork using plots saved above

girafe(ggobj = multipanels, #render multi-panel plot with ggiraph
       width_svg = 10,  #adjust width
       height_svg = 6, #adjust height
       options = list(opts_hover(css = "fill:skyblue;stroke:black;cursor:pointer;"), #customize fill and outline of hover
                      opts_tooltip = opts_tooltip(css = "padding:3px;background-color:white;color:black;") #customize size and colors of text label and background when hovering
       )
    )

What did we learn?

In this tutorial, we:

  1. Learned a workflow to create interactive plots in R to clarify complex data:
  • Make static visualizations and ensure plots follow data visualization principles

  • Build interactive plots with plotly (Sievert, 2020) and ggiraph (Gohel & Skintzos, 2025) that can be customized depending on components of interest

  1. Created interactive plots in R to display data of interest for tier 1 English Women’s Football league teams (The English Women’s Football Database, 2025), such as…
  • Line plots showing teams’ averages on home game attendance by season

  • Barplots and stacked barplots showing teams’ performance (goal difference and points) within seasons

  • Rank plots showing teams’ positions over seasons

  • Scatter plots showing associations between teams’ home game attendance and score margin across seasons

  1. Polished a deliverable-ready, interactive multi-paneled plot

Happy plotting!

References & Additional Resources

References

R Core Team (2025). R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing, Vienna, Austria. https://www.R-project.org/.

The English Women’s Football (EWF) Database. (2025). https://github.com/probjects/ewf-database

Wickham H, Averick M, Bryan J, Chang W, McGowan LD, François R, Grolemund G, Hayes A, Henry L, Hester J, Kuhn M, Pedersen TL, Miller E, Bache SM, Müller K, Ooms J, Robinson D, Seidel DP, Spinu V, Takahashi K, Vaughan D, Wilke C, Woo K, Yutani H (2019). “Welcome to the tidyverse.” Journal of Open Source Software, 4(43), 1686. doi:10.21105/joss.01686 https://doi.org/10.21105/joss.01686.

C. Sievert. Interactive Web-Based Data Visualization with R, plotly, and shiny. Chapman and Hall/CRC Florida, 2020.

Sjoberg D (2020). ggbump: Bump Chart and Sigmoid Curves. doi:10.32614/CRAN.package.ggbump https://doi.org/10.32614/CRAN.package.ggbump, R package version 0.1.0, https://CRAN.R-project.org/package=ggbump.

Silge J, Robinson D (2016). “tidytext: Text Mining and Analysis Using Tidy Data Principles in R.” JOSS, 1(3). doi:10.21105/joss.00037 https://doi.org/10.21105/joss.00037, http://dx.doi.org/10.21105/joss.00037.

Gohel D, Skintzos P (2025). ggiraph: Make ‘ggplot2’ Graphics Interactive. doi:10.32614/CRAN.package.ggiraph https://doi.org/10.32614/CRAN.package.ggiraph, R package version 0.9.1, https://CRAN.R-project.org/package=ggiraph.

Pedersen T (2024). patchwork: The Composer of Plots. doi:10.32614/CRAN.package.patchwork https://doi.org/10.32614/CRAN.package.patchwork, R package version 1.3.0, https://CRAN.R-project.org/package=patchwork.

Mills BR (2022). MetBrewer: Color Palettes Inspired by Works at the Metropolitan Museum of Art. doi:10.32614/CRAN.package.MetBrewer https://doi.org/10.32614/CRAN.package.MetBrewer, R package version 0.2.0, https://CRAN.R-project.org/package=MetBrewer.

Allaire J, Xie Y, Dervieux C, McPherson J, Luraschi J, Ushey K, Atkins A, Wickham H, Cheng J, Chang W, Iannone R (2024). rmarkdown: Dynamic Documents for R. R package version 2.29, https://github.com/rstudio/rmarkdown.

Xie Y, Allaire J, Grolemund G (2018). R Markdown: The Definitive Guide. Chapman and Hall/CRC, Boca Raton, Florida. ISBN 9781138359338, https://bookdown.org/yihui/rmarkdown.

Xie Y, Dervieux C, Riederer E (2020). R Markdown Cookbook. Chapman and Hall/CRC, Boca Raton, Florida. ISBN 9780367563837, https://bookdown.org/yihui/rmarkdown-cookbook.

Xie Y (2025). knitr: A General-Purpose Package for Dynamic Report Generation in R. R package version 1.50, https://yihui.org/knitr/.

Yihui Xie (2015) Dynamic Documents with R and knitr. 2nd edition. Chapman and Hall/CRC. ISBN 978-1498716963

Yihui Xie (2014) knitr: A Comprehensive Tool for Reproducible Research in R. In Victoria Stodden, Friedrich Leisch and Roger D. Peng, editors, Implementing Reproducible Computational Research. Chapman and Hall/CRC. ISBN 978-1466561595

Posit team (2025). RStudio: Integrated Development Environment for R. Posit Software, PBC, Boston, MA. URL http://www.posit.co/.

## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value
##  version  R version 4.5.0 (2025-04-11)
##  os       macOS Sequoia 15.6
##  system   aarch64, darwin20
##  ui       X11
##  language (EN)
##  collate  en_US.UTF-8
##  ctype    en_US.UTF-8
##  tz       America/New_York
##  date     2025-12-12
##  pandoc   3.4 @ /Applications/RStudio.app/Contents/Resources/app/quarto/bin/tools/aarch64/ (via rmarkdown)
##  quarto   1.6.42 @ /Applications/RStudio.app/Contents/Resources/app/quarto/bin/quarto
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package      * version date (UTC) lib source
##  bit            4.6.0   2025-03-06 [1] CRAN (R 4.5.0)
##  bit64          4.6.0-1 2025-01-16 [1] CRAN (R 4.5.0)
##  bslib          0.9.0   2025-01-30 [1] CRAN (R 4.5.0)
##  cachem         1.1.0   2024-05-16 [1] CRAN (R 4.5.0)
##  cli            3.6.5   2025-04-23 [1] CRAN (R 4.5.0)
##  crayon         1.5.3   2024-06-20 [1] CRAN (R 4.5.0)
##  crosstalk      1.2.1   2023-11-23 [1] CRAN (R 4.5.0)
##  data.table     1.17.2  2025-05-12 [1] CRAN (R 4.5.0)
##  dichromat      2.0-0.1 2022-05-02 [1] CRAN (R 4.5.0)
##  digest         0.6.37  2024-08-19 [1] CRAN (R 4.5.0)
##  dplyr        * 1.1.4   2023-11-17 [1] CRAN (R 4.5.0)
##  evaluate       1.0.3   2025-01-10 [1] CRAN (R 4.5.0)
##  farver         2.1.2   2024-05-13 [1] CRAN (R 4.5.0)
##  fastmap        1.2.0   2024-05-15 [1] CRAN (R 4.5.0)
##  forcats      * 1.0.0   2023-01-29 [1] CRAN (R 4.5.0)
##  generics       0.1.4   2025-05-09 [1] CRAN (R 4.5.0)
##  ggbump       * 0.1.0   2020-04-24 [1] CRAN (R 4.5.0)
##  ggiraph      * 0.9.1   2025-09-16 [1] CRAN (R 4.5.0)
##  ggplot2      * 4.0.0   2025-09-11 [1] CRAN (R 4.5.0)
##  glue           1.8.0   2024-09-30 [1] CRAN (R 4.5.0)
##  gtable         0.3.6   2024-10-25 [1] CRAN (R 4.5.0)
##  hms            1.1.3   2023-03-21 [1] CRAN (R 4.5.0)
##  htmltools      0.5.8.1 2024-04-04 [1] CRAN (R 4.5.0)
##  htmlwidgets    1.6.4   2023-12-06 [1] CRAN (R 4.5.0)
##  httr           1.4.7   2023-08-15 [1] CRAN (R 4.5.0)
##  janeaustenr    1.0.0   2022-08-26 [1] CRAN (R 4.5.0)
##  jquerylib      0.1.4   2021-04-26 [1] CRAN (R 4.5.0)
##  jsonlite       2.0.0   2025-03-27 [1] CRAN (R 4.5.0)
##  knitr          1.50    2025-03-16 [1] CRAN (R 4.5.0)
##  labeling       0.4.3   2023-08-29 [1] CRAN (R 4.5.0)
##  lattice        0.22-6  2024-03-20 [1] CRAN (R 4.5.0)
##  lazyeval       0.2.2   2019-03-15 [1] CRAN (R 4.5.0)
##  lifecycle      1.0.4   2023-11-07 [1] CRAN (R 4.5.0)
##  lubridate    * 1.9.4   2024-12-08 [1] CRAN (R 4.5.0)
##  magrittr       2.0.3   2022-03-30 [1] CRAN (R 4.5.0)
##  Matrix         1.7-3   2025-03-11 [1] CRAN (R 4.5.0)
##  MetBrewer    * 0.2.0   2022-03-21 [1] CRAN (R 4.5.0)
##  mgcv           1.9-1   2023-12-21 [1] CRAN (R 4.5.0)
##  mnormt         2.1.1   2022-09-26 [1] CRAN (R 4.5.0)
##  nlme           3.1-168 2025-03-31 [1] CRAN (R 4.5.0)
##  patchwork    * 1.3.0   2024-09-16 [1] CRAN (R 4.5.0)
##  pillar         1.11.0  2025-07-04 [1] CRAN (R 4.5.0)
##  pkgconfig      2.0.3   2019-09-22 [1] CRAN (R 4.5.0)
##  plotly       * 4.11.0  2025-06-19 [1] CRAN (R 4.5.0)
##  psych          2.5.3   2025-03-21 [1] CRAN (R 4.5.0)
##  purrr        * 1.0.4   2025-02-05 [1] CRAN (R 4.5.0)
##  R6             2.6.1   2025-02-15 [1] CRAN (R 4.5.0)
##  RColorBrewer   1.1-3   2022-04-03 [1] CRAN (R 4.5.0)
##  Rcpp           1.0.14  2025-01-12 [1] CRAN (R 4.5.0)
##  readr        * 2.1.5   2024-01-10 [1] CRAN (R 4.5.0)
##  rlang          1.1.6   2025-04-11 [1] CRAN (R 4.5.0)
##  rmarkdown      2.29    2024-11-04 [1] CRAN (R 4.5.0)
##  rmcorr       * 0.7.0   2024-07-26 [1] CRAN (R 4.5.0)
##  rstudioapi     0.17.1  2024-10-22 [1] CRAN (R 4.5.0)
##  S7             0.2.0   2024-11-07 [1] CRAN (R 4.5.0)
##  sass           0.4.10  2025-04-11 [1] CRAN (R 4.5.0)
##  scales         1.4.0   2025-04-24 [1] CRAN (R 4.5.0)
##  sessioninfo    1.2.3   2025-02-05 [1] CRAN (R 4.5.0)
##  SnowballC      0.7.1   2023-04-25 [1] CRAN (R 4.5.0)
##  stringi        1.8.7   2025-03-27 [1] CRAN (R 4.5.0)
##  stringr      * 1.5.1   2023-11-14 [1] CRAN (R 4.5.0)
##  systemfonts    1.2.3   2025-04-30 [1] CRAN (R 4.5.0)
##  tibble       * 3.3.0   2025-06-08 [1] CRAN (R 4.5.0)
##  tidyr        * 1.3.1   2024-01-24 [1] CRAN (R 4.5.0)
##  tidyselect     1.2.1   2024-03-11 [1] CRAN (R 4.5.0)
##  tidytext     * 0.4.2   2024-04-10 [1] CRAN (R 4.5.0)
##  tidyverse    * 2.0.0   2023-02-22 [1] CRAN (R 4.5.0)
##  timechange     0.3.0   2024-01-18 [1] CRAN (R 4.5.0)
##  tokenizers     0.3.0   2022-12-22 [1] CRAN (R 4.5.0)
##  tzdb           0.5.0   2025-03-15 [1] CRAN (R 4.5.0)
##  utf8           1.2.6   2025-06-08 [1] CRAN (R 4.5.0)
##  uuid           1.2-1   2024-07-29 [1] CRAN (R 4.5.0)
##  vctrs          0.6.5   2023-12-01 [1] CRAN (R 4.5.0)
##  viridisLite    0.4.2   2023-05-02 [1] CRAN (R 4.5.0)
##  vroom          1.6.5   2023-12-05 [1] CRAN (R 4.5.0)
##  withr          3.0.2   2024-10-28 [1] CRAN (R 4.5.0)
##  xfun           0.52    2025-04-02 [1] CRAN (R 4.5.0)
##  yaml           2.3.10  2024-07-26 [1] CRAN (R 4.5.0)
## 
##  [1] /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
##  * ── Packages attached to the search path.
## 
## ──────────────────────────────────────────────────────────────────────────────